<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>JS系列之闭包</title>
  <link rel="icon" type="image/png" href="../think/assets/ant.png">
  <link href="../styles.css" rel="stylesheet">
  <link href="../vs.css" rel="stylesheet">
  <!-- Global site tag (gtag.js) - Google Analytics -->
  <script async src="https://www.googletagmanager.com/gtag/js?id=UA-131906119-1"></script>
  <script>
    window.dataLayer = window.dataLayer || [];
    function gtag(){dataLayer.push(arguments);}
    gtag('js', new Date());

    gtag('config', 'UA-131906119-1');
  </script>
  <script async src="../index.js"></script>
</head>
<body>
<h1>JS系列之闭包</h1>
<h2>目录</h2>
<ul>
<li>前言</li>
<li>作用域</li>
<li>JS中的this</li>
<li>JS中的闭包</li>
<li>使用闭包注意事项</li>
<li>参考</li>
</ul>
<p><strong>涉及到的名词</strong>：闭包、作用域、自由变量、本地变量、词法作用域（静态作用域）、语法作用域</p>
<h2>前言</h2>
<p>本文的目的是试图讲清楚JS中的闭包（closure）概念。简单说，闭包由函数和函数定义处的作用域（scope）组成。在JS中，当函数被调用时，函数体里面的自由变量（free variable）是去函数定义处的作用域中查找，而不是去函数被调用处的作用域中查找。</p>
<p>那为什么函数中的自由变量不去函数被调用处的作用域中查找呢？这是因为JS使用的作用域机制是词法作用域（lexical scoping），而不是动态作用域（dynamic scoping）。</p>
<p>那为什么JS要使用词法作用域而不是动态作用域呢？这是因为这样更符合人的思维。人在写代码的时候，只能看到函数的定义内容，无法看到函数被调用的过程。因为这个原因，人在查找一个函数中的自由变量时，自然是去函数定义处往外查找。</p>
<h2>作用域</h2>
<p>当程序运行引擎看到一个变量的引用时，它会去作用域中把所绑定（name binding）的值找出来。如果把引擎看作一个人，那么作用域就是这个人的视野，这个人只能去这个视野中查找某个名称所对应的物体。所以呢，作用域其实就是一个<code>[变量-&gt;值, ...]</code>（<code>-&gt;</code>表示某变量与某值绑定）的列表，引擎遇到一个变量时，就在这个列表里面寻找变量所对应的值。需要注意的是，作用域之间可以嵌套。当变量没有在最近的作用域中找到所绑定的值时，会往上到包裹该作用域的作用域中查找，直到最顶层作用域为止。</p>
<p>在ES5及之前的JS标准中，主要存在全局作用域和函数作用域两种。比如以下代码形成了三个作用域：</p>
<pre><code class="language-js"><span class="hljs-keyword">var</span> a = <span class="hljs-number">1</span>;
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">f1</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">var</span> b = <span class="hljs-number">2</span>;
  <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">f2</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">var</span> c = <span class="hljs-number">3</span>;
    <span class="hljs-built_in">console</span>.log(a + b + c);
  }
  f2();
}
f1();
</code></pre>
<p>其中最外层是全局作用域S1，内容为<code>[a-&gt;1, ...]</code>。函数f1形成了作用域S2，内容为<code>[b-&gt;2, ..., &lt;outer&gt;-&gt;S1]</code>。函数f2形成了作用域S2，内容为<code>[c-&gt;3, ..., &lt;outer&gt;-&gt;S2]</code>。这里的<code>&lt;outer&gt;</code>表示该作用域的外层作用域，变量没有在当前作用域中找到时，就会去外层作用域中查找。</p>
<p>当运行到f2中的<code>console.log(a + b + c);</code>时，运行引擎会去作用域S3中查找变量a、b、c，没有找到的话就会到它的外层，直到最外层作用域为止，如果最外层还没有找到，就会报not defined错误。这样，a在作用域S1中被找到，b在S2中被找到，c在S3中被找到，因此在console中会打印出<code>1 + 2 + 3</code>的计算结果。</p>
<p>在最近的ES6/7的JS标准中，除了已有的var，引入了新的let和const关键字来声明变量，这两种声明方式的一个变化就是引入了块级作用域。比如在if语句中通过var声明的变量存在于离它最近的全局或者函数作用域中，而let和const声明的变量则存在于由if语句块所组成的作用域中。</p>
<h2>JS中的this</h2>
<p>在每一个全局或者函数作用域中，除了通过声明变量显式的添加变量到作用域，还存在一个隐式添加的变量this，并且在块级作用域中则不存在。全局作用域中的this指向的是全局作用域本身，而函数作用域中的this，则根据函数被调用的方式来决定其所指向的值。也就是说this的指向只能到运行时确定。可以这样简单来理解，每当函数被调用时，会形成一个函数作用域，并默默地添加this变量到该作用域中去。</p>
<p>至于函数每次被调用时this指向何值，有以下几种情况：</p>
<ol>
<li>纯函数形式调用：例如<code>f();</code>，此时this在非严格模式下指向全局变量，严格模式下为<code>undefined</code></li>
<li>通过对象点属性形式来调用：例如<code>obj1.method();</code>，此时this指向该对象<code>obj1</code></li>
<li>通过call、apply、bind来调用：例如<code>f.call(obj2);</code>，此时this指向第一个参数<code>obj2</code></li>
<li>通过new操作符来调用：例如<code>new F();</code>，此时this指向一个空对象，并且该空对象的原型指向<code>F.prototype</code></li>
</ol>
<p>因此，如果将this补充到前文中的几个作用域中去，则S1的内容为<code>[a-&gt;1, this-&gt;S1, ...]</code>，S2的内容为<code>[b-&gt;2, this-&gt;S1, ..., &lt;outer&gt;-&gt;S1]</code>，S3的内容为<code>[c-&gt;3, this-&gt;S1, ..., &lt;outer&gt;-&gt;S1]</code>。</p>
<p>因为这个多变的this，JS中的作用域实际上并不是很纯粹的词法作用域。纯粹的词法作用域应该在代码静态分析过程中就可以确定作用域中每个变量的指向。</p>
<h2>JS中的闭包</h2>
<p>现在请你根据上面的信息，思考以下问题：</p>
<p>为了保准函数执行的正确性，除了需要知道函数体的内容，运行引擎还需要知道什么东西呢？</p>
<p>请先思考几分钟再看下文的解答。</p>
<p>答案是还需要知道函数定义时所处的作用域，否则引擎不知道去哪里寻找函数体中的自由变量。其实可以换句话来说，那就是<strong>闭包就是函数</strong>，只是编译器遇到函数定义时，会默默地将定义时所处的作用域与之绑定。这使得这个函数不管在源码的哪个位置（哪个作用域）执行，引擎都是从定义时的作用域中寻找自由变量所绑定的值。</p>
<p>这样看来，闭包在所有使用词法作用域机制的编程语言中都存在。那为什么在JS中如此强调闭包呢？我觉得这是因为JS中的对象没有私有属性功能。私有属性有非常重要的好处：一来这样可以限制使用者只能通过调用标准的接口，而不是直接访问和修改属性，可以实现使用方式与具体实现无关；二来可以保护隐私数据不被获取到。</p>
<p>跑到JS这里来，就只能绕个弯子，间接地通过闭包来达到这个目的了:(</p>
<p>方式就是将函数作用域（ES6后块级作用域也行）里面的函数暴露出去，外面的使用者就只能通过暴露的函数来访问和修改这个作用域里面的变量了。</p>
<h2>使用闭包注意事项</h2>
<p>执行以下代码可以得到下面的内存实时图（延时只是为了便于观察，无特殊用途）：</p>
<pre><code class="language-html"><span class="hljs-meta">&lt;!DOCTYPE html&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
    <span class="hljs-comment">// 分配n个100兆内存</span>
    <span class="hljs-keyword">var</span> alloc_100 = <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">n</span>) </span>{
        <span class="hljs-keyword">var</span> arr = [];
        <span class="hljs-keyword">for</span> (<span class="hljs-keyword">var</span> i = <span class="hljs-number">0</span>; i &lt; n; i += <span class="hljs-number">1</span>) {
            arr.push(<span class="hljs-keyword">new</span> <span class="hljs-built_in">Array</span>(<span class="hljs-number">10e7</span>).join(<span class="hljs-string">'*'</span>));
        }
        <span class="hljs-keyword">return</span> arr;
    };
    <span class="hljs-keyword">var</span> f;
    setTimeout(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
        <span class="hljs-keyword">var</span> data = alloc_100(<span class="hljs-number">10</span>);
        <span class="hljs-keyword">var</span> xxx = <span class="hljs-literal">true</span>;
        f = <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
            <span class="hljs-keyword">if</span> (xxx) {
                <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'hi'</span>);
            }
        };
        setTimeout(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
            f = <span class="hljs-literal">null</span>;
        }, <span class="hljs-number">5000</span>);
    }, <span class="hljs-number">5000</span>);
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre>
<p><img src="../think/assets/019-closure.jpg" alt="闭包使内存不被泄漏"></p>
<p>上面代码所展示的是，当<code>f</code>保有对函数的引用并且函数体中函数对作用域的变量(<code>xxx</code>)有使用时，垃圾回收机制（garbage collection）不会回收这个作用域，包括里面的<code>data</code>变量。只有将<code>f</code>置为空，垃圾回收机制才能回收。</p>
<p>因此在使用闭包时，对于不需要的闭包应该将所有对其的引用都消除掉，使得GC能够回收。</p>
<h2>参考</h2>
<ul>
<li><a href="https://en.wikipedia.org/wiki/Scope_(computer_science)">wiki: Scope</a></li>
<li><a href="https://en.wikipedia.org/wiki/Closure_(computer_programming)">wiki: Closure</a></li>
<li><a href="https://developer.mozilla.org/cn/docs/Web/JavaScript/Closures">MDN: JavaScript - Closures</a></li>
<li><a href="http://stackoverflow.com/questions/111102/how-do-javascript-closures-work">Stack Overflow: How do JavaScript closures work?</a></li>
<li><a href="https://auth0.com/blog/four-types-of-leaks-in-your-javascript-code-and-how-to-get-rid-of-them/">4 Types of Memory Leaks in JavaScript and How to Get Rid Of Them</a></li>
</ul>


</body>
</html>
